package fr.adrienbrault.idea.symfony2plugin.util;

import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.php.codeInsight.PhpCodeInsightUtil;
import com.jetbrains.php.lang.PhpLangUtil;
import com.jetbrains.php.lang.documentation.phpdoc.PhpDocUtil;
import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.*;
import de.espend.idea.php.annotation.util.AnnotationUtil;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
 * Some method from Php Annotations plugin to not fully set a "depends" entry on it
 *
 * @author Daniel Espendiller <daniel@espendiller.net>
 */
public class AnnotationBackportUtil {

    public static final Set<String> NON_ANNOTATION_TAGS = new HashSet<>() {{
        addAll(Arrays.asList(PhpDocUtil.ALL_TAGS));
        add("@Annotation");
        add("@inheritDoc");
        add("@Enum");
        add("@inheritdoc");
        add("@Target");
    }};

    @Nullable
    public static PhpClass getAnnotationReference(PhpDocTag phpDocTag, final Map<String, String> useImports) {

        String tagName = phpDocTag.getName();
        if(tagName.startsWith("@")) {
            tagName = tagName.substring(1);
        }

        String className = tagName;
        String subNamespaceName = "";
        if(className.contains("\\")) {
            className = className.substring(0, className.indexOf("\\"));
            subNamespaceName = tagName.substring(className.length());
        }

        if(!useImports.containsKey(className)) {
            return null;
        }

        return PhpElementsUtil.getClass(phpDocTag.getProject(), useImports.get(className) + subNamespaceName);

    }

    @NotNull
    public static Map<String, String> getUseImportMap(@NotNull PhpDocTag phpDocTag) {
        PhpDocComment phpDoc = PsiTreeUtil.getParentOfType(phpDocTag, PhpDocComment.class);
        if(phpDoc == null) {
            return Collections.emptyMap();
        }

        return getUseImportMap(phpDoc);
    }

    @NotNull
    public static Map<String, String> getUseImportMap(@NotNull PhpDocComment phpDocComment) {

        // search for use alias in local file
        final Map<String, String> useImports = new HashMap<>();

        PhpPsiElement scope = PhpCodeInsightUtil.findScopeForUseOperator(phpDocComment);
        if(scope == null) {
            return useImports;
        }

        for (PhpUseList phpUseList : PhpCodeInsightUtil.collectImports(scope)) {
            for (PhpUse phpUse : phpUseList.getDeclarations()) {
                String alias = phpUse.getAliasName();
                useImports.put(Objects.requireNonNullElseGet(alias, phpUse::getName), phpUse.getFQN());
            }
        }

        return useImports;
    }

    @NotNull
    public static Collection<PhpDocTag> filterValidDocTags(Collection<PhpDocTag> phpDocTags) {

        Collection<PhpDocTag> filteredPhpDocTags = new ArrayList<>();

        for(PhpDocTag phpDocTag: phpDocTags) {
            if(!NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
                filteredPhpDocTags.add(phpDocTag);
            }
        }

        return filteredPhpDocTags;
    }

    public static boolean hasReference(@Nullable PhpDocComment docComment, String... className) {
        if(docComment == null) return false;

        Map<String, String> uses = AnnotationBackportUtil.getUseImportMap(docComment);

        for(PhpDocTag phpDocTag: PsiTreeUtil.findChildrenOfAnyType(docComment, PhpDocTag.class)) {
            if(!AnnotationBackportUtil.NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
                PhpClass annotationReference = AnnotationBackportUtil.getAnnotationReference(phpDocTag, uses);
                if(annotationReference != null && PhpElementsUtil.isEqualClassName(annotationReference, className)) {
                    return true;
                }
            }

        }

        return false;
    }

    public static PhpDocTag getReference(@Nullable PhpDocComment docComment, String className) {
        if(docComment == null) return null;

        Map<String, String> uses = AnnotationBackportUtil.getUseImportMap(docComment);

        for(PhpDocTag phpDocTag: PsiTreeUtil.findChildrenOfAnyType(docComment, PhpDocTag.class)) {
            if(AnnotationBackportUtil.NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
                continue;
            }

            PhpClass annotationReference = AnnotationBackportUtil.getAnnotationReference(phpDocTag, uses);
            if(annotationReference != null && PhpElementsUtil.isEqualClassName(annotationReference, className)) {
                return phpDocTag;
            }
        }

        return null;
    }

    /**
     * Get class path on "use" path statement
     */
    @Nullable
    public static String getQualifiedName(@NotNull PsiElement psiElement, @NotNull String fqn) {

        PhpPsiElement scopeForUseOperator = PhpCodeInsightUtil.findScopeForUseOperator(psiElement);
        if (scopeForUseOperator == null) {
            return null;
        }

        PhpReference reference = PhpPsiUtil.getParentByCondition(psiElement, false, PhpReference.INSTANCEOF);
        String qualifiedName = PhpCodeInsightUtil.createQualifiedName(scopeForUseOperator, fqn, reference, false);
        if (!PhpLangUtil.isFqn(qualifiedName)) {
            return qualifiedName;
        }

        // @TODO: remove full fqn fallback
        if(qualifiedName.startsWith("\\")) {
            qualifiedName = qualifiedName.substring(1);
        }

        return qualifiedName;
    }

    /**
     * "AppBundle\Controller\DefaultController::fooAction" => app_default_foo"
     * "Foo\ParkResortBundle\Controller\SubController\BundleController\FooController::nestedFooAction" => foo_parkresort_sub_bundle_foo_nestedfoo"
     */
    @Nullable
    public static String getRouteByMethod(@NotNull PhpDocTag phpDocTag) {
        Method method = getMethodScope(phpDocTag);
        if (method == null) {
            return null;
        }

        return getRouteByMethod(method);
    }

    public static String getRouteByMethod(@NotNull Method method) {
        String name = method.getName();

        // strip action
        if(name.endsWith("Action")) {
            name = name.substring(0, name.length() - "Action".length());
        }

        PhpClass containingClass = method.getContainingClass();
        if(containingClass == null) {
            return null;
        }

        String[] fqn = org.apache.commons.lang3.StringUtils.strip(containingClass.getFQN(), "\\").split("\\\\");

        // remove empty and controller only namespace
        List<String> filter = ContainerUtil.filter(fqn, s ->
            org.apache.commons.lang3.StringUtils.isNotBlank(s) && !"controller".equalsIgnoreCase(s)
        );

        if(filter.isEmpty()) {
            return null;
        }

        return org.apache.commons.lang3.StringUtils.join(ContainerUtil.map(filter, s -> {
            String content = s.toLowerCase();
            if (content.endsWith("bundle") && !content.equalsIgnoreCase("bundle")) {
                return content.substring(0, content.length() - "bundle".length());
            }
            if (content.endsWith("controller") && !content.equalsIgnoreCase("controller")) {
                return content.substring(0, content.length() - "controller".length());
            }
            return content;
        }), "_") + (!name.startsWith("_") ? "_" : "") + name.toLowerCase();
    }

    /**
     * Generate a route path/URL from the method and controller name.
     *
     * "App\Controller\ProductController::indexAction" => "/product"
     * "App\Controller\ProductController::showAction" => "/product/show"
     * "App\Controller\Admin\UserController::editAction" => "/admin/user/edit"
     */
    @Nullable
    public static String getRoutePathByMethod(@NotNull Method method) {
        String methodName = method.getName();

        // strip "Action" suffix
        if (methodName.endsWith("Action")) {
            methodName = methodName.substring(0, methodName.length() - "Action".length());
        }

        PhpClass containingClass = method.getContainingClass();
        if (containingClass == null) {
            return null;
        }

        String[] fqnParts = org.apache.commons.lang3.StringUtils.strip(containingClass.getFQN(), "\\").split("\\\\");

        // filter and transform namespace parts
        List<String> pathParts = new ArrayList<>();
        boolean foundController = false;

        for (String part : fqnParts) {
            if (org.apache.commons.lang3.StringUtils.isBlank(part)) {
                continue;
            }

            String lowerPart = part.toLowerCase();

            // skip everything before "controller" namespace
            if ("controller".equalsIgnoreCase(part)) {
                foundController = true;
                continue;
            }

            if (!foundController) {
                continue;
            }

            // strip "Controller" suffix from class name
            if (lowerPart.endsWith("controller")) {
                pathParts.add(lowerPart.substring(0, lowerPart.length() - "controller".length()));
            } else {
                pathParts.add(lowerPart);
            }
        }

        if (pathParts.isEmpty()) {
            return null;
        }

        // add method name if not "index"
        if (!"index".equalsIgnoreCase(methodName)) {
            pathParts.add(methodName.toLowerCase());
        }

        return "/" + org.apache.commons.lang3.StringUtils.join(pathParts, "/");
    }

    @Nullable
    public static Method getMethodScope(@NotNull PhpDocTag phpDocTag) {
        PhpDocComment parentOfType = PsiTreeUtil.getParentOfType(phpDocTag, PhpDocComment.class);
        if(parentOfType == null) {
            return null;
        }

        PhpPsiElement method = parentOfType.getNextPsiSibling();
        if(!(method instanceof Method)) {
            return null;
        }

        return (Method) method;
    }

    @Nullable
    public static String getClassNameReference(PhpDocTag phpDocTag, Map<String, String> useImports) {

        if(useImports.isEmpty()) {
            return null;
        }

        String annotationName = phpDocTag.getName();
        if(StringUtils.isBlank(annotationName)) {
            return null;
        }

        if(annotationName.startsWith("@")) {
            annotationName = annotationName.substring(1);
        }

        String className = annotationName;
        String subNamespaceName = "";
        if(className.contains("\\")) {
            className = className.substring(0, className.indexOf("\\"));
            subNamespaceName = annotationName.substring(className.length());
        }

        if(!useImports.containsKey(className)) {
            return null;
        }

        // normalize name
        String annotationFqnName = useImports.get(className) + subNamespaceName;
        if(!annotationFqnName.startsWith("\\")) {
            annotationFqnName = "\\" + annotationFqnName;
        }

        return annotationFqnName;
    }

    /**
     * Generate a full FQN class name out of a given short class name with respecting current namespace and use scope
     *
     * - "Foobar" needs to have its use statement attached
     * - No use statement match its on the same namespace as the class
     *
     * TODO: find a core function for this
     *
     * @param shortClassName Foobar
     */
    @NotNull
    public static String getFqnClassNameFromScope(@NotNull PhpClass phpClass, @NotNull String shortClassName, @NotNull Map<String, String> useImportMap) {
        String classNameScope = phpClass.getFQN();
        if(!classNameScope.startsWith("\\")) {
            classNameScope = "\\" + classNameScope;
        }

        // its already on the global namespace: "\Exception"
        if (shortClassName.startsWith("\\")) {
            return shortClassName;
        }

        // "Foo\Bar" split it on "subnamespace"; if no "subnamespace" only care about the first array item as out use match
        String[] split = shortClassName.split("\\\\");
        if (useImportMap.containsKey(split[0])) {
            String shortClassImport = useImportMap.get(split[0]);

            // on "Foo\Bar" we must extend also "Bar" for the import
            // "Foo\Bar" => "\Car\Foo\Bar"
            if (split.length > 1) {
                String[] yourArray = Arrays.copyOfRange(split, 1, split.length);
                shortClassImport += "\\" + StringUtils.join(yourArray, "\\");
            }

            return shortClassImport;
        }

        // strip the last namespace part and replace it with ours: "Foobar\Bar" => "Foobar\OurShortClass"
        return StringUtils.substringBeforeLast(classNameScope, "\\") + "\\" + shortClassName;
    }

    /**
     * Generate a full FQN class name out of a given short class name with respecting current namespace and use scope
     *
     * - "Foobar" needs to have its use statement attached
     * - No use statement match its on the same namespace as the class
     *
     * TODO: find a core function for this
     *
     * @param shortClassName Foobar
     */
    @NotNull
    public static String getFqnClassNameFromScope(@NotNull PhpClass phpClass, @NotNull String shortClassName) {
        // @TODO: better scope? we dont need a doc comment
        PhpDocComment docComment = phpClass.getDocComment();
        if (docComment == null) {
            return shortClassName;
        }

        return getFqnClassNameFromScope(phpClass, shortClassName, AnnotationUtil.getUseImportMap(docComment));
    }
}
